In this exercise we will learn the basics of the implementation of a physically-based material.
Remember that you can find hints in comments in the code, tagged with the label "(todo)" followed by the number of the exercise.
Before we start implementing our PBR shaders, it is important to take a quick look to the new additions to the library:
We need to replace the current Blinn-Phong shader program with a new one.
We are reading the material properties from textures that we will reuse in both. You already now the color and normal textures, but we will introduce a new one, ARM, named with the initials of the components packed in it:
We can reuse the same vertex shader, but we need some changes to the fragment shader:
If you run the program now, you should have a very similar result as before, but with only the ambient and diffuse terms.
Finally, let's replace the fixed ambient indirect diffuse lighting with something more PBR. One of the goals of PBR is to reduce the need to tweak materials when lighting conditions change. For this exercise, since we don't have any GI (global illumination), we will use the skybox to get the environment contribution.
In the lighting model library, lambert-ggx.glsl, inside the ComputeDiffuseIndirectLighting function, replace the fixed value (0.25f) with a sample from the environment texture, in the direction of the surface normal. You can use the helper function SampleEnvironment for this. For the LOD level (mipmap), we use the one with less detail, passing a value of 1 to the function.
You can set the light intensity to 0 to visualize only the contribution of the environment.
Try to load other models included in the exercise to validate the solution.
In the Blinn-Phong lighting model all the indirect lighting is modelled by a single ambient value, with nothing specific for specular.
In this exercise we will add some environment reflection to model the indirect specular contribution, sampling from the skybox texture.
Find the ComputeSpecularIndirectLighting function in the lambert-ggx.glsl shader, and add a sample from the environment, using the same function as in the previous section.
For the direction, this time we won't use the normal map. Instead, find the reflected view vector on the normal of the surface. You can use the reflect GLSL function. The view direction used in lighting goes from the surface to the camera, but in this case you have to use the inverse. Use a LOD level of 0, for now.
If you did the calculation correctly, the object should look like a mirror! On thing required to improve this is to change the LOD level depending on the roughness.
Smooth surfaces will have a more detailed reflection (lower LOD level), while rough surfaces will have a blurred one (higher LOD level). You can use the roughness value, already in the 0-1 range, as the LOD level. However, because the mips have not been processed properly to contain smooth values, we will push the values towards 1. Use the following expression as your LOD level: pow(data.roughness, 0.25f).
It should look better now, but there is still obviously too much indirect lighting. We will deal with that in the next exercise.
One of the principles of PBR is energy conservation. At the moment, we are adding the indirect light twice, as if it was reflected in all directions but also in the reflected direction (diffuse + specular).
To select between these two, we will use the Schlick approximation of the fresnel term. Look for the equation in the slides and implement it in lambert-ggx.glsl.
In CombineIndirectLighting, compute the value of the fresnel, and use it to mix between diffuse and specular, instead of adding them together.
You can obtain the F0 value using the GetReflectance function. The 2 vectors that you need to provide to the function are the viewDir and the normal, instead of the halfDir.
Does it look better now? Thanks to the fresnel term, the indirect will be most visible when the view direction is more perpendicular to the normal.
Follow the same procedure to mix diffuse and specular in CombineLighting, using the same reflectance, and the viewDir and halfDir vectors this time.
Now that the indirect lighting is working fine, we can move to direct lighting.
For the diffuse, we saw in the lecture some more complex models, but we will keep using the simple Lambertian model, adapted to PBR:
The visual results should be very similar to the previous exercise.
This is a good time to try the different models included in the exercise and see how the light is affecting them.
Until now, we didn't have any specular in this model. This is because the equations are completely different from the Blinn-Phong model, and it was not worth it to keep anything.
The first step towards our specular model is implementing the Cook-Torrance equation:
Both implementations of the D and G terms are initially empty. In this exercise we will only implement the distribution function.
Find the DistributionGGX function and implement it, following the slides. The alpha factor used in the equation corresponds to our roughness.
You may notice some high specular values on some grazing angles. This looks incorrect, and will be solved by the geometry term in the next exercise.
As we discussed before, we need to implement the geometry term to account for shadowing and masking of the microsurfaces.
We are using the Smith equation that divides the G2 term in two separate G1 functions (one for shadowing, one for masking). This is already implemented for you, in the function GeometrySmith.
However, you still have to implement the equation for the G1 function for GGX, GeometrySchlickGGX. With the help of the slides, implement this equation and observe the results.
With this, the direct specular term is finally complete. But we can also improve the quality of the render by applying this same function to the indirect specular lighting. Multiply the specular indirect in ComputeSpecularIndirectLightingLighting with a call to GeometrySmith function, using reflectionDir as the input direction, instead of lightDir.
Our PBR model for dielectrics is finished now!
There is only one small (but important) part to finish now.
Until now we have ignored metals completely, but they don't follow the same rendering model.
Metalness will affect two parts of our model:
Now we can say that the PBR model is finished. You should load all the different models in the exercise and look around them with the camera, observing the different materials.
The renderer we are using is too simple, and there are multiple options to improve it: